package com.ruoyi.common.utils;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/**
 * Base64加解密算法
 * </p>
 * Base64加密算法：<br/>
 * 1、获取字符串中每个字符的ASCII码；<br/>
 * 2、按照每3个8bit的字符为一组来分组，即每组24bit；<br/>
 * 3、将这24bit划分成4个6bit的4个单位，每个单位前面添加2个0，得到4个8bit的单位；<br/>
 * 4、将每个8bit的单位转换成十进制数字，对照Base64编码表找到对应的字符进行拼接，得到最终的加密后的字符串。<br/>
 * </p>
 * Base64解密算法：<br/>
 * 1、读入4个字符，对照Base64编码表找到字符对应的索引，生成4个6为的值；<br/>
 * 2、将这4个6为的值拼接起来，形成一个24为的值；<br/>
 * 3、将这个24位的值按照8位一组截断成3个8位的值；<br/>
 * 4、对照ASCII表找到这三个8位的值对应的字符，即解码后的字符。<br/>
 * </p>
 * 注意事项：<br/>
 * 1、被编码的字符必须是8bit的，即必须在ASCII码范围内，中文不行；<br/>
 * 2、如果被编码的字符长度不是3的倍数，则在最后添加1或2个0，对应的输出字符为“=”；
 * 3、给定一个字符串，用Base64方法对其进行加密后解密，得到的结果就不是开始时候的字符串了。<br/>
 */
public final class Base64Util {
	
	private static final Charset charset = StandardCharsets.UTF_8;  // 编码格式
	
	private static final int BASELENGTH = 128;
	private static final int LOOKUPLENGTH = 64;
	private static final int TWENTYFOURBITGROUP = 24;
	private static final int EIGHTBIT = 8;
	private static final int SIXTEENBIT = 16;
	private static final int FOURBYTE = 4;
	private static final int SIGN = -128;
	private static final char PAD = '=';
	private static final byte[] base64Alphabet = new byte[BASELENGTH];
	private static final char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH];

	static {
		for (int i = 0; i < BASELENGTH; ++i) {
			base64Alphabet[i] = -1;
		}
		for (int i = 'Z'; i >= 'A'; i--) {
			base64Alphabet[i] = (byte) (i - 'A');
		}
		for (int i = 'z'; i >= 'a'; i--) {
			base64Alphabet[i] = (byte) (i - 'a' + 26);
		}
		for (int i = '9'; i >= '0'; i--) {
			base64Alphabet[i] = (byte) (i - '0' + 52);
		}
		base64Alphabet['-'] = 62;
		base64Alphabet['_'] = 63;
		for (int i = 0; i <= 25; i++) {
			lookUpBase64Alphabet[i] = (char) ('A' + i);
		}
		for (int i = 26, j = 0; i <= 51; i++, j++) {
			lookUpBase64Alphabet[i] = (char) ('a' + j);
		}
		for (int i = 52, j = 0; i <= 61; i++, j++) {
			lookUpBase64Alphabet[i] = (char) ('0' + j);
		}
		lookUpBase64Alphabet[62] = (char) '-';
		lookUpBase64Alphabet[63] = (char) '_';
	}

	private Base64Util() {}

	/**
	 * 判断是否为 Base64 编码
	 */
	public static boolean isBase64(String str) {
		if(str == null || "".equals(str)) return false;
		for(int i = 0; i < str.length(); i++) {
			if(!isBase64(str.charAt(i))) return false;
		}
		return true;
	}
	
	/**
	 * 使用 BASE64 进行加密
	 */
	public static String encode(String origin) {
		if (origin == null || "".equals(origin)) return origin;
		return encode(origin.getBytes(charset));
	}

	/**
	 * 使用 BASE64 进行加密
	 */
	public static String encode(byte[] binaryData) {
		if (binaryData == null) return null;
		if (binaryData.length == 0) return "";
		int lengthDataBits = binaryData.length * EIGHTBIT;
		if (lengthDataBits == 0) return "";
		int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
		int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
		int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets;
		char encodedData[] = null;
		encodedData = new char[numberQuartet * 4];
		byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;
		int encodedIndex = 0;
		int dataIndex = 0;
		for (int i = 0; i < numberTriplets; i++) {
			b1 = binaryData[dataIndex++];
			b2 = binaryData[dataIndex++];
			b3 = binaryData[dataIndex++];
			l = (byte) (b2 & 0x0f);
			k = (byte) (b1 & 0x03);
			byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
			byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
			byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc);
			encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
			encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
			encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3];
			encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f];
		}
		// form integral number of 6-bit groups
		if (fewerThan24bits == EIGHTBIT) {
			b1 = binaryData[dataIndex];
			k = (byte) (b1 & 0x03);
			byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
			encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
			encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4];
			encodedData[encodedIndex++] = PAD;
			encodedData[encodedIndex++] = PAD;
		} else if (fewerThan24bits == SIXTEENBIT) {
			b1 = binaryData[dataIndex];
			b2 = binaryData[dataIndex + 1];
			l = (byte) (b2 & 0x0f);
			k = (byte) (b1 & 0x03);
			byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
			byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
			encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
			encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
			encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2];
			encodedData[encodedIndex++] = PAD;
		}
		return new String(encodedData);
	}

	/**
	 * 使用 BASE64 进行解密
	 */
	public static String decode(String encoded) {
		byte[] decoded = decodeToBytes(encoded);
		return decoded == null ? null : new String(decoded, charset);
	}
	
	/**
	 * 使用 BASE64 进行解密（解密为byte数组）
	 */
	public static byte[] decodeToBytes(String encoded) {
		if (encoded == null) return null;
		if ("".equals(encoded)) return "".getBytes();
		if (!isBase64(encoded)) throw new RuntimeException("Not Base64 Code!");
		char[] base64Data = encoded.toCharArray();
		// remove white spaces
		int len = removeWhiteSpace(base64Data);
		if (len % FOURBYTE != 0) return null;// should be divisible by four
		int numberQuadruple = (len / FOURBYTE);
		if (numberQuadruple == 0) return new byte[0];
		byte decodedData[] = null;
		byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
		char d1 = 0, d2 = 0, d3 = 0, d4 = 0;
		int i = 0;
		int encodedIndex = 0;
		int dataIndex = 0;
		decodedData = new byte[(numberQuadruple) * 3];
		for (; i < numberQuadruple - 1; i++) {
			if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))
					|| !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++])))
				return null;  // if found "no data" just return null
			b1 = base64Alphabet[d1];
			b2 = base64Alphabet[d2];
			b3 = base64Alphabet[d3];
			b4 = base64Alphabet[d4];
			decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
			decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
			decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
		}
		if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) {
			return null;  // if found "no data" just return null
		}
		b1 = base64Alphabet[d1];
		b2 = base64Alphabet[d2];
		d3 = base64Data[dataIndex++];
		d4 = base64Data[dataIndex++];
		if (!isData((d3)) || !isData((d4))) {  // Check if they are PAD characters
			if (isPad(d3) && isPad(d4)) {  // Two PAD e.g. 3c[Pad][Pad]
				if ((b2 & 0xf) != 0) return null;  // last 4 bits should be zero				
				byte[] tmp = new byte[i * 3 + 1];
				System.arraycopy(decodedData, 0, tmp, 0, i * 3);
				tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
				return tmp;
			} else if (!isPad(d3) && isPad(d4)) {  // One PAD e.g. 3cQ[Pad]
				b3 = base64Alphabet[d3];
				if ((b3 & 0x3) != 0) return null;  // last 2 bits should be zero
				byte[] tmp = new byte[i * 3 + 2];
				System.arraycopy(decodedData, 0, tmp, 0, i * 3);
				tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
				tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
				return tmp;
			} else {
				return null;  // an error like "3c[Pad]r", "3cdX", "3cXd", "3cXX" , where X is non data
			}
		} else { // No PAD e.g 3cQl
			b3 = base64Alphabet[d3];
			b4 = base64Alphabet[d4];
			decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
			decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
			decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
		}
		return decodedData;
	}
	
	private static boolean isWhiteSpace(char octect) {
		return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
	}

	private static boolean isPad(char octect) {
		return (octect == PAD);
	}

	private static boolean isData(char octect) {
		return (octect < BASELENGTH && base64Alphabet[octect] != -1);
	}

	private static boolean isBase64(char octect) {
		return (isWhiteSpace(octect) || isPad(octect) || isData(octect));
	}

    private static int removeWhiteSpace(char[] data) {
        if (data == null) return 0;
        int newSize = 0;
        int len = data.length;
        for (int i = 0; i < len; i++) {
            if (!isWhiteSpace(data[i])) data[newSize++] = data[i];
        }
        return newSize;
    }
    
}